import 'dart:convert';

import 'package:serverpod/serverpod.dart';

import '../../../core.dart';

/// {@template secret_challenge_util}
/// Utility class for creating and verifying secret challenges.
///
/// This class provides the core challenge mechanism that can be reused across
/// different authentication flows that require secret challenges (e.g. email
/// registration verification, password reset verification, passwordless login).
///
/// The challenge flow follows a two-step verification pattern:
/// 1. Create a challenge with a verification code sent to the user.
/// 2. Verify the code and create a completion token.
/// 3. Use the completion token to complete the operation.
/// {@endtemplate}
class SecretChallengeUtil<T> {
  final Argon2HashUtil _hashUtil;
  final SecretChallengeVerificationConfig<T> _verificationConfig;
  final SecretChallengeCompletionConfig<T> _completionConfig;

  /// Creates a new [SecretChallengeUtil] instance.
  ///
  /// [verificationConfig] is the configuration for verifying challenges.
  /// [completionConfig] is the configuration for completing challenges.
  /// [hashUtil] is the utility for hashing verification codes.
  SecretChallengeUtil({
    required final Argon2HashUtil hashUtil,
    required final SecretChallengeVerificationConfig<T> verificationConfig,
    required final SecretChallengeCompletionConfig<T> completionConfig,
  }) : _verificationConfig = verificationConfig,
       _completionConfig = completionConfig,
       _hashUtil = hashUtil;

  /// Creates a new [SecretChallenge] from a verification code.
  ///
  /// This method:
  /// 1. Generates the hash of [verificationCode]
  /// 2. Creates and inserts a [SecretChallenge] record
  ///
  /// Returns the created [SecretChallenge] with its database ID.
  ///
  /// The [verificationCode] should be generated by the caller and sent to the
  /// user via the appropriate channel (email, SMS, etc.).
  Future<SecretChallenge> createChallenge(
    final Session session, {
    required final String verificationCode,
    required final Transaction transaction,
  }) async {
    final verificationCodeHash = await _hashUtil.createHashFromString(
      secret: verificationCode,
    );

    return SecretChallenge.db.insertRow(
      session,
      SecretChallenge(
        challengeCodeHash: verificationCodeHash,
      ),
      transaction: transaction,
    );
  }

  /// Verifies a verification code against a [SecretChallenge].
  ///
  /// Throws:
  /// - [ChallengeRequestNotFoundException] if the request is not found.
  /// - [ChallengeAlreadyUsedException] if the challenge has already been used.
  /// - [ChallengeInvalidVerificationCodeException] if the verification code is invalid.
  /// - [ChallengeExpiredException] if the request has expired.
  /// - [ChallengeRateLimitExceededException] if the rate limit is exceeded.
  ///
  /// Returns the encoded completion token that can be used to complete the
  /// operation.
  Future<String> verifyChallenge(
    final Session session, {
    required final UuidValue requestId,
    required final String verificationCode,
    required final Transaction transaction,
  }) async {
    final config = _verificationConfig;

    if (await config.hasTooManyAttempts(
      session,
      nonce: requestId,
    )) {
      throw ChallengeRateLimitExceededException();
    }

    final request = await config.getRequest(
      session,
      requestId,
      transaction: transaction,
    );
    if (request == null) {
      throw ChallengeRequestNotFoundException();
    }

    if (config.isAlreadyUsed(request)) {
      throw ChallengeAlreadyUsedException();
    }

    final challenge = config.getChallenge(request);

    if (!await _validateVerificationCode(
      verificationCode: verificationCode,
      challenge: challenge,
    )) {
      throw ChallengeInvalidVerificationCodeException();
    }

    if (config.isExpired(request)) {
      await config.onExpired(session, request);
      throw ChallengeExpiredException();
    }

    final completionTokenResult = await _createCompletionToken(
      session,
      requestId: requestId,
      transaction: transaction,
    );

    await config.linkCompletionToken(
      session,
      request,
      completionTokenResult.challenge,
      transaction: transaction,
    );

    return completionTokenResult.encodedToken;
  }

  /// Validates a completion token with all protection mechanisms.
  ///
  /// This method handles the complete completion token validation flow including:
  /// - Token decoding
  /// - Request retrieval
  /// - Not-verified detection (completion challenge missing)
  /// - Code verification
  /// - Expiration checks
  ///
  /// Throws:
  /// - [ChallengeInvalidCompletionTokenException] if the token format is invalid.
  /// - [ChallengeRequestNotFoundException] if the request is not found.
  /// - [ChallengeAlreadyUsedException] if the request has not been verified yet.
  /// - [ChallengeInvalidVerificationCodeException] if the verification code is invalid.
  /// - [ChallengeExpiredException] if the request has expired.
  ///
  /// Returns the validated request that can be used to complete the operation.
  Future<T> completeChallenge(
    final Session session, {
    required final String completionToken,
    required final Transaction transaction,
  }) async {
    final config = _completionConfig;

    final credentials = _decodeCompletionToken(completionToken);

    if (await config.hasTooManyAttempts(
      session,
      nonce: credentials.requestId,
    )) {
      throw ChallengeRateLimitExceededException();
    }

    final request = await config.getRequest(
      session,
      credentials.requestId,
      transaction: transaction,
    );
    if (request == null) {
      throw ChallengeRequestNotFoundException();
    }

    final completionChallenge = config.getCompletionChallenge(request);
    if (completionChallenge == null) {
      throw ChallengeNotVerifiedException();
    }

    if (!await _validateVerificationCode(
      verificationCode: credentials.verificationCode,
      challenge: completionChallenge,
    )) {
      throw ChallengeInvalidVerificationCodeException();
    }

    if (config.isExpired(request)) {
      await config.onExpired(session, request);
      throw ChallengeExpiredException();
    }

    return request;
  }

  /// Creates a completion token after successful challenge verification.
  ///
  /// The returned [CompletionTokenResult] contains:
  /// - [CompletionTokenResult.challenge]: The created [SecretChallenge] that
  ///   should be linked to the request original.
  /// - [CompletionTokenResult.encodedToken]: A base64-encoded token containing
  ///   the request ID and the raw token, which should be returned to the client.
  ///
  /// The caller is responsible for linking the challenge to their request
  /// record (e.g., setting `createAccountChallengeId` or `setPasswordChallengeId`).
  Future<CompletionTokenResult> _createCompletionToken(
    final Session session, {
    required final UuidValue requestId,
    required final Transaction transaction,
  }) async {
    final token = const Uuid().v4();
    final tokenHash = await _hashUtil.createHashFromString(secret: token);

    final challenge = await SecretChallenge.db.insertRow(
      session,
      SecretChallenge(challengeCodeHash: tokenHash),
      transaction: transaction,
    );

    return CompletionTokenResult(
      challenge: challenge,
      encodedToken: _encodeCompletionToken(requestId, token),
    );
  }

  /// Validates a verification code against a [SecretChallenge].
  ///
  /// Returns `true` if the code matches the challenge hash, `false` otherwise.
  ///
  /// This method only validates the hash - it does not check expiration, rate
  /// limits, or other protection mechanisms. Those should be handled by the
  /// caller before invoking this method.
  Future<bool> _validateVerificationCode({
    required final String verificationCode,
    required final SecretChallenge challenge,
  }) async {
    return _hashUtil.validateHashFromString(
      secret: verificationCode,
      hashString: challenge.challengeCodeHash,
    );
  }

  String _encodeCompletionToken(final UuidValue requestId, final String token) {
    return base64Encode(utf8.encode('$requestId:$token'));
  }

  /// Decodes and returns the credentials from a completion token.
  ///
  /// This method only decodes the token - validation against the stored
  /// challenge should be done by calling [_validateVerificationCode] with the
  /// extracted verification code.
  ///
  /// Throws [ChallengeInvalidCompletionTokenException] if the token format is
  /// malformed or invalid.
  ///
  /// The returned [CompletionTokenCredentials] contains:
  /// - [CompletionTokenCredentials.requestId]: The ID of the request that this
  ///   token belongs to.
  /// - [CompletionTokenCredentials.verificationCode]: The raw verification code
  ///   to validate against the challenge.
  CompletionTokenCredentials _decodeCompletionToken(final String token) {
    final String decoded;
    try {
      decoded = utf8.decode(base64Decode(token));
    } catch (e) {
      throw ChallengeInvalidCompletionTokenException();
    }

    final parts = decoded.split(':');
    if (parts.length != 2) throw ChallengeInvalidCompletionTokenException();
    final verificationCode = parts[1];

    final UuidValue requestId;
    try {
      requestId = UuidValue.withValidation(parts[0]);
    } catch (e) {
      throw ChallengeInvalidCompletionTokenException();
    }

    return CompletionTokenCredentials(
      requestId: requestId,
      verificationCode: verificationCode,
    );
  }
}

/// Result of creating a completion token.
class CompletionTokenResult {
  /// The created challenge that should be linked to the request.
  final SecretChallenge challenge;

  /// The base64-encoded token to return to the client.
  final String encodedToken;

  /// Creates a new [CompletionTokenResult].
  const CompletionTokenResult({
    required this.challenge,
    required this.encodedToken,
  });
}

/// Decoded credentials from a completion token.
class CompletionTokenCredentials {
  /// The ID of the request this token belongs to.
  final UuidValue requestId;

  /// The raw verification code to validate against the challenge.
  final String verificationCode;

  /// Creates new [CompletionTokenCredentials].
  const CompletionTokenCredentials({
    required this.requestId,
    required this.verificationCode,
  });
}
